package cn.claim.storage.client;


import cn.claim.storage.tools.*;

import java.io.*;
import java.net.Socket;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Scanner;
import java.util.UUID;

/**
 * Created by claim on 17-7-7.
 */
public final class FileClient {
    /**
     * raise a request to node server and get info of the file to be uploaded
     */
    private static long checkFileStatus(String md5, String uuid, Socket node) {
        int retry = 5;
        while (true) {
            try {
                PrintWriter request = new PrintWriter(node.getOutputStream());
                Scanner response = new Scanner(node.getInputStream());

                request.println("check");
                request.println(new ClientQueryData(md5, uuid).toJSON());
                request.flush();
                String length = response.nextLine();
                return Long.parseLong(length);
            } catch (IOException | NoSuchElementException e) {
                if (retry == 0)
                    break;
                System.out.println("未能连接节点, 稍后将进行重试");
                retry--;
                try {
                    Thread.sleep(5000);
                    //reconnect by a new socket
                    node.close();
                    node = new Socket(node.getInetAddress(), node.getPort());
                } catch (InterruptedException | IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
        return 0;
    }

    /**
     * @return Similar to {@link #upload(String, NodeData, String)}, return -1 when failed.
     */
    private static int writeFileContent(File file, Socket node, long offset) {
        FileUploader uploader = new FileUploader(file, node);
        return uploader.upload(offset);
    }

    /**
     * Transport the encoded file from a existed .base64 file
     */
    private static boolean transport(File uploaded, NodeData node) {
        Socket transport = null;
        String uuid = uploaded.getName(), md5 = DataTools.calculateMd5(uploaded);
        int retry = 5;
        while (true) {
            try {
                transport = new Socket(node.getIp(), node.getUploadPort());
                PrintWriter request = new PrintWriter(transport.getOutputStream());
                Scanner response = new Scanner(transport.getInputStream());

                request.println(new ClientQueryData(md5, uuid).toJSON());
                request.println(uploaded.length());
                request.flush();

                long offset = Long.parseLong(response.nextLine());
                if (offset == -1)
                    return false;
                if (writeFileContent(uploaded, transport, offset) > 0)
                    break;
                transport.close();
                retry = 5;
            } catch (IOException | NoSuchElementException e) {
                try {
                    retry--;
                    if (retry == 0)
                        return false;
                    System.out.println("连接已断开，将进行重试");
                    Thread.sleep(5000);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            } finally {
                try {
                    if (transport != null) {
                        transport.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return true;
    }

    private static File upload(String filePath, NodeData node, String encryptId) {
        File file = new File(filePath);
        if (!file.exists())
            throw new IllegalArgumentException("指定的待上传文件不存在!");
        file = Crypter.encryptFile(file, encryptId);
        String uuid = UUID.randomUUID().toString();
        File uploaded = new File(file.getParent() + "/" + uuid);
        //noinspection ResultOfMethodCallIgnored
        file.renameTo(uploaded);
        try {
            System.out.println(file.getCanonicalPath() + " is uploading...");
        } catch (IOException e) {
            e.printStackTrace();
        }
        int retry = 5;
        while (true) {
            if (!transport(uploaded, node))
                return null;
            try {
                Socket query = new Socket(node.getIp(), node.getQueryPort());
                String md5 = DataTools.calculateMd5(uploaded);
                System.out.println("文件传输完毕，稍后将进行校验...");
                Thread.sleep(5000);
                long offset = checkFileStatus(md5, uuid, query);
                if (offset == uploaded.length()) {
                    System.out.println("文件传输成功");
                    System.out.printf("uuid: %s\n", uuid);
                    break;
                } else {
                    System.out.println("文件传输失败, 稍后重试");
                    if (--retry == 0)
                        return null;
                }
            } catch (IOException e) {
                System.out.println("连接已断开");
                if (--retry == 0)
                    return null;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return uploaded;
    }

    private static boolean download(String id, FileInfoData fileInfo, NodeData node, File target) {
        Socket downloadSocket = null;
        PrintWriter request;
        Scanner response;
        try {
            while (true) {
                downloadSocket = new Socket(node.getIp(), node.getDownloadPort());
                request = new PrintWriter(downloadSocket.getOutputStream());
                response = new Scanner(downloadSocket.getInputStream());

                request.println(fileInfo.getUuid());
                request.flush();

                if (Objects.equals(response.nextLine(), "null"))
                    return false;

                FileAccepter accepter = new FileAccepter(target, downloadSocket);
                TransportingData log = accepter.logger.getData();
                long range;
                if (log != null)
                    range = log.getRange();
                else {
                    accepter.logger.log(fileInfo.getMd5(), 0);
                    range = 0;
                }
                request.println(range);
                request.flush();

                int status = accepter.accept(range);
                if (status > 0) {
                    accepter.logger.removeDataFile();
                    //check file, if fail restart download
                    if (target.length() == fileInfo.getLength()
                            && Objects.equals(DataTools.calculateMd5(target), fileInfo.getMd5())) {
                        break;
                    }
                }
                downloadSocket.close();
            }
            //noinspection ResultOfMethodCallIgnored
        } catch (IOException e) {
            //e.printStackTrace();
            return false;
        } finally {
            try {
                if (downloadSocket != null) {
                    downloadSocket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return true;
    }

    public static void main(String[] args) {
//        NodeData node = NodeDataBuilder.newInstance()
//                .withDownloadPort(7000)
//                .withIp("localhost")
//                .withUploadPort(6000)
//                .withQueryPort(8000)
//                .withSpareSpace(1000000)
//                .withName("node1").build();
        if (args.length > 0) {
            InfoHandler infoHandler = new InfoHandler();
            switch (args[0]) {
                case "upload": {
                    NodeData theNode = infoHandler.getNodeInfo((NodeData) null);
                    File uploaded = new File(args[1]), temp = null;
                    if (!uploaded.exists()) {
                        System.out.println("文件不存在");
                        return;
                    }
                    int time = 3;
                    String key = args.length > 2 ? args[2] : "default";
                    while ((theNode == null || (temp = upload(args[1], theNode, key)) == null) && time != 0) {
                        theNode = infoHandler.getNodeInfo(theNode);
                        time--;
                    }
                    if (time == 0)
                        System.out.println("未找到合适节点上传，可能是由于网络不畅或无空闲节点");
                    else {
                        infoHandler.registerFile(FileInfoDataBuilder.newInstance()
                                .withFileName(uploaded.getName())
                                .withLength(temp.length())
                                .withBackupServerName("")
                                .withMd5(DataTools.calculateMd5(temp))
                                .withServerName(theNode.getName())
                                .withUuid(temp.getName())
                                .build());
                    }
                    System.out.println("备份完毕");
                    //todo exit earlier
                    //no more wait
                    break;
                }
                case "download": {
                    FileInfoData fileInfoData = infoHandler.getFileInfo(args[1]);
                    File temp = null;
                    try {
                        temp = File.createTempFile("___", ".base64");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    String key = args.length > 3 ? args[3] : "default";

                    //noinspection ConstantConditions
                    if (fileInfoData == null) {
                        System.out.println("该文件不存在");
                        return;
                    }
                    NodeData nodeData = infoHandler.getNodeInfo(fileInfoData.getServerName()), backupNodeData = null;
                    boolean finished = false;
                    for (int i = 0; i < 5; i++) {
                        if (nodeData == null || !download(key, fileInfoData, nodeData, temp)) {
                            System.out.println("主节点失去连接，正在尝试连接备份节点");
                            if (backupNodeData == null)
                                backupNodeData = infoHandler.getNodeInfo(fileInfoData.getBackupServerName());
                            if (backupNodeData != null && download(key, fileInfoData, backupNodeData, temp)) {
                                finished = true;
                                break;
                            }
                            if (i < 2)
                                System.out.println("备份节点失去连接，稍后将重新尝试");
                            try {
                                Thread.sleep(5000);
                                nodeData = infoHandler.getNodeInfo(fileInfoData.getServerName());
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        } else {
                            finished = true;
                            break;
                        }
                    }
                    if (!finished) {
                        System.out.println("重试次数过多，下载失败");
                        return;
                    }
                    File target = new File((args[2].charAt(args[2].length() - 1) != '/' ? args[2] += "/" : args[2])
                            + fileInfoData.getFileName());
                    try {
                        target.createNewFile();
                    } catch (IOException e) {
                        System.out.println("该文件已存在");
                        e.printStackTrace();
                        return;
                    }
                    Crypter.decryptFile(temp, key, target);
                    System.out.println("文件下载完毕");
                    break;
                }
                case "remove":
                    String removeId = args[1];
                    infoHandler.deleteFile(removeId);
                    break;
                case "help":
                    System.out.println("可用命令列表：");
                    System.out.println("upload ${文件名} [${加密秘钥}]");
                    System.out.println("download ${uuid} ${目标目录} [${加密秘钥}]");
                    System.out.println("remove ${uuid}");
                    break;
                default:
                    System.out.println("参数无效!");
            }
        }
    }
}
